package engine.core;

import engine.core.scene.Scene;
import engine.sound.Musicplayer;
import engine.sound.SoundManager;
import engine.util.draw.Draw2D;
import engine.util.error.Check;
import engine.util.texture.TextureLoader;
import engine.util.timer.Timer;

import java.util.ArrayList;

import javax.swing.JOptionPane;

import org.lwjgl.LWJGLException;
import org.lwjgl.input.Keyboard;
import org.lwjgl.input.Mouse;
import org.lwjgl.opengl.ContextAttribs;
import org.lwjgl.opengl.Display;
import org.lwjgl.opengl.DisplayMode;
import org.lwjgl.opengl.PixelFormat;

import static org.lwjgl.opengl.GL11.*;

/**
 * This abstract class provides a basic resizeable frame, containing one openGL
 * window. This window includes window decoration but does not support
 * fullscreen mode.
 * 
 * @author richard
 * 
 */
public abstract class OpenGLView implements Runnable {
	private ArrayList<Scene> scenes;
	private Scene currentScene;
	private Scene newCurrentScene;

	private int width;
	private int height;

	private String title;

	private boolean drag;

	private float timeElapsed;

	private int frames;
	private float timeToWait;

	private boolean isRunning;

	private int majorOpenGLVersion = 3;
	private int minorOpenGLVersion = 3;
	
	//profiler information
	private int fps;
	private float swapBufferTime;

	/**
	 * Setting up an openGL window with the specified title.
	 * 
	 * @param title
	 */
	public OpenGLView(String title) {
		this.title = title;
		scenes = new ArrayList<Scene>();
	}

	/**
	 * Setting up an openGL window with the specified title and size.
	 * 
	 * @param title
	 * @param width
	 * @param height
	 */
	public OpenGLView(String title, int width, int height) {
		this(title);
		this.width = width;
		this.height = height;
	}

	public OpenGLView(String title, int width, int height,
			int majorOpenGLVersion, int minorOpenGLVersion) {
		this(title, width, height);

		this.majorOpenGLVersion = majorOpenGLVersion;
		this.minorOpenGLVersion = minorOpenGLVersion;
	}

	/**
	 * This method is the core function of the engine and is running the game
	 * loop.
	 */
	@Override
	public void run() {
		// At first, create the openGL window.
		createOpenGLWindow();
		
		Check.error("Core created opengl window: ");

		// Initialize the standards, that are set all over the engine.
		initPresets();
		
		Check.error("Core init presets: ");

		// Call init, for initialize setup.
		init();
		
		Check.error("Core subclass init: ");

		// Calling init on any added scene.
		for (Scene s : scenes) {
			s.initAbs();
		}

		// Calling reshape once, that the screen can be resized.
		reshape();

		// initialize timing variables for fps measurement
		timeElapsed = Timer.getTime();
		timeToWait = Timer.getTime() + 1000;

		isRunning = true;

		// starting game loop.
		while (isRunning && !Display.isCloseRequested()) {
			// this occurs, if a new scene is set. The change of scenes can only
			// be applied in the game loop.
			if (newCurrentScene != null) {
				if (currentScene != null) {
					currentScene.unsetCurrent();
				}
				currentScene = newCurrentScene;
				currentScene.setCurrentAbs();
				newCurrentScene = null;
			}

			// Checks, if display size changed. If it change, the reshape method
			// must be called and the scenes needs to be updated.
			if (Display.wasResized()) {
				width = Display.getWidth();
				height = Display.getHeight();

				glViewport(0, 0, getWidth(), getHeight());

				for (Scene s : scenes) {
					s.updateView(width, height);
				}

				reshape();
			}

			// handles the input
			manageInput();

			// fps time measurement and rendering.
			float lastTimeElapsed = timeElapsed;
			timeElapsed = Timer.getTime();
			float t = timeElapsed - lastTimeElapsed;
			render(t);

			// render the current scene
			if (currentScene != null) {
				currentScene.renderAbs(t);
			}
			
			// update Musicplayer
			Musicplayer.getInstance().update(t);

			frames++;

			if (timeToWait < Timer.getTime()) {
				timeToWait = Timer.getTime() + 1000;
				Display.setTitle(title + " - " + frames + " FPS");
				fps = frames;
				frames = 0;
			}

			float time = Timer.getTime();
			// Swap the buffers and show the changes on the screen.
			Display.update();
			reduceInputLag();
			
			swapBufferTime = Timer.getTime() - time;
			
			// End game loop.
		}
		
		//unset current scene
		if(currentScene != null) {
			currentScene.unsetCurrent();
		}

		// Clear any scene.
		for (Scene s : scenes) {
			s.exit();
		}

		//tidy up render stuff
		Draw2D.destroy();
		// Clear the sound buffers
		SoundManager.destroy();
		
		// Clear the program itself.
		dealloc();
		TextureLoader.dealloc();
		Display.destroy();
	}

	private void initPresets() {
		Draw2D.init();
		SoundManager.init();
	}

	/**
	 * Polling the events while waiting for vsync.
	 */
	private void reduceInputLag() {
		// check, if an error occured and print it
		Check.error("Main loop");
		Display.processMessages();
		Mouse.poll();
		Keyboard.poll();
	}

	/**
	 * Handles the mouse and key input of the openGL window.
	 */
	private void manageInput() {
		while (Keyboard.next()) {
			boolean keyState = Keyboard.getEventKeyState();
			int eventKey = Keyboard.getEventKey();

			if (keyState) {
				keyPressed(eventKey);
				if (currentScene != null) {
					currentScene.keyPressedAbs(eventKey);
				}
			} else {
				keyReleased(eventKey);
				if (currentScene != null) {
					currentScene.keyReleasedAbs(eventKey);
				}
			}

		}

		while (Mouse.next()) {
			boolean buttonState = Mouse.getEventButtonState();
			int x = Mouse.getX();
			int y = Mouse.getY();
			int eventButton = Mouse.getEventButton();
			int wheel = Mouse.getEventDWheel();

			if (buttonState) {
				drag = true;
				mousePressed(x, y, eventButton);
				if (currentScene != null) {
					currentScene.mousePressedAbs(x, y, eventButton);
				}
			} else {
				if (Mouse.getEventButton() < 0) {
					if (drag) {
						mouseDragged(x, y, eventButton);
						if (currentScene != null) {
							currentScene.mouseDraggedAbs(x, y, eventButton);
						}
					} else {
						mouseMoved(x, y, eventButton);
						if (currentScene != null) {
							currentScene.mouseMovedAbs(x, y, eventButton);
						}
					}
				} else {
					drag = false;
					mouseReleased(x, y, eventButton);
					if (currentScene != null) {
						currentScene.mouseReleasedAbs(x, y, eventButton);
					}
				}
			}

			mouseWheelMoved(wheel / 120);
			if(currentScene != null && wheel != 0) {
				currentScene.mouseWheelMovedAbs(wheel / 120);
			}
		}
	}

	/**
	 * Creates the openGL window, which is placed within the canvas.
	 * 
	 * @param canvas
	 */
	private void createOpenGLWindow() {
		try {
			Display.setFullscreen(false);
			Display.setResizable(true);
			Display.setVSyncEnabled(true);
			Display.setTitle(title);
			Keyboard.enableRepeatEvents(true);

			if (width != 0 && height != 0) {
				Display.setDisplayMode(getDisplayMode(width, height));
			} else {
				Display.setDisplayMode(Display.getDesktopDisplayMode());
			}
			
			//Create display
			PixelFormat pixelFormat = new PixelFormat();
			ContextAttribs contextAttribute = new ContextAttribs(majorOpenGLVersion, minorOpenGLVersion).withForwardCompatible(true).withProfileCore(true);
			
			Display.create(pixelFormat, contextAttribute);

			// check, if the correct version is supported
			checkVersion();

			// check, if we use the best graphics card
			checkGraphicsCard();

			width = Display.getWidth();
			height = Display.getHeight();
		} catch (LWJGLException e) {
			System.err.println("Error while creating the Display");
			System.exit(0);
		}
	}

	private void checkVersion() {
		String versionString = glGetString(GL_VERSION);

		String[] split1 = versionString.split(" ");
		String[] split2 = split1[0].split("\\.");

		int major = Integer.parseInt(split2[0]);
		int minor = Integer.parseInt(split2[1]);

		if (major < majorOpenGLVersion
				|| (major == majorOpenGLVersion && minor < minorOpenGLVersion)) {
			int confirm = JOptionPane.showConfirmDialog(null,
					"Your system seems to not support the requested opengl version "
							+ majorOpenGLVersion + "." + minorOpenGLVersion
							+ ". Do you want to start anyway?", "Wrong openGL version",
					JOptionPane.YES_NO_OPTION, JOptionPane.INFORMATION_MESSAGE,
					null);

			if (confirm == JOptionPane.NO_OPTION) {
				Display.destroy();
				System.exit(0);
			}
		}
	}

	private void checkGraphicsCard() {
		String card = glGetString(GL_RENDERER);

		System.out.println(card);
	}

	private DisplayMode getDisplayMode(int width, int height)
			throws LWJGLException {
		DisplayMode desktop = Display.getDesktopDisplayMode();
		ArrayList<DisplayMode> usableModes = new ArrayList<DisplayMode>();

		for (DisplayMode d : Display.getAvailableDisplayModes()) {
			if (d.getWidth() == width && d.getHeight() == height) {
				usableModes.add(d);
			}
		}

		if (usableModes.size() == 0) {
			return desktop;
		} else {
			DisplayMode best = usableModes.get(0);

			for (DisplayMode d : usableModes) {
				if (best.getBitsPerPixel() == desktop.getBitsPerPixel()) {
					if (Math.abs(desktop.getFrequency() - best.getFrequency()) > Math
							.abs(desktop.getFrequency() - d.getFrequency())) {
						best = d;
					}
				}
			}

			return best;
		}
	}

	public int getWidth() {
		return width;
	}

	public int getHeight() {
		return height;
	}

	public void setRunning(boolean isRunning) {
		this.isRunning = isRunning;
	}

	public boolean isRunning() {
		return isRunning;
	}

	/**
	 * Is called, directly after the openGL window is created.
	 */
	public void init() {
	};

	/**
	 * Is called any time, the openGL window is resized.
	 */
	public void reshape() {
	};

	/**
	 * Is called any time, when the openGL window renders.
	 * 
	 * @param timeElapsed
	 */
	public void render(float timeElapsed) {
	};

	/**
	 * Is called, when the key is pressed.
	 * 
	 * @param key
	 */
	public void keyPressed(int key) {
	};

	/**
	 * Is called, when the key is released.
	 * 
	 * @param key
	 */
	public void keyReleased(int key) {
	};

	/**
	 * Is called, when the mouse is pressed.
	 * 
	 * @param posX
	 * @param posY
	 * @param button
	 */
	public void mousePressed(int posX, int posY, int button) {
	}

	/**
	 * Is called, when the mouse is released.
	 * 
	 * @param posX
	 * @param posY
	 * @param button
	 */
	public void mouseReleased(int posX, int posY, int button) {
	}

	/**
	 * Is called, when the mouse is moved.
	 * 
	 * @param posX
	 * @param posY
	 * @param button
	 */
	public void mouseMoved(int posX, int posY, int button) {
	}

	/**
	 * Is called, when the mouse is dragged.
	 * 
	 * @param posX
	 * @param posY
	 * @param button
	 */
	public void mouseDragged(int posX, int posY, int button) {
	};

	/**
	 * Is called, when the mouse wheel was moved.
	 * 
	 * @param movement
	 */
	public void mouseWheelMoved(int movement) {
	};

	/**
	 * Is called, directly before the openGL window is destroyed.
	 */
	public void dealloc() {
	};

	public void addScene(Scene scene) {
		scene.setOpenGLView(this);
		scenes.add(scene);
	}

	public void removeScene(Scene scene) {
		scenes.remove(scene);
	}

	public Scene getCurrentScene() {
		return currentScene;
	}

	public void setCurrentScene(Scene currentScene) {
		newCurrentScene = currentScene;
	}

	public void setCurrentScene(String name) {
		for (Scene s : scenes) {
			if (s.getName() != null && s.getName().equals(name)) {
				newCurrentScene = s;
				return;
			}
		}

		throw new IllegalArgumentException("There is no scene with the name: "
				+ name);
	}
	
	public int getFps() {
		return fps;
	}
	
	public float getSwapBufferTime() {
		return swapBufferTime;
	}
}
